package de.jmda.core.mproc.task;

import static java.lang.System.currentTimeMillis;
import static java.lang.System.lineSeparator;

import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import javax.annotation.processing.SupportedAnnotationTypes;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;

import org.apache.commons.lang3.StringUtils;

import de.jmda.core.util.JavaCompilerUtil;
import de.jmda.core.util.fileset.FileSet;
import de.jmda.core.util.ramcomp.RAMCompiler;
import de.jmda.core.util.ramcomp.RAMCompiler.RAMCompilerData;

public abstract class TaskRunnerAnnotations
{
//	private final static Logger LOGGER =
//      Logger.getLogger(TaskRunnerAnnotations.class);

	public static void run(AbstractAnnotatedElementsTask... tasks) throws IOException
	{
		run(null, tasks);
	}

	public static void run(
			Iterable<String> options,
			AbstractAnnotatedElementsTask... tasks) throws IOException
	{
		if (options == null) options = new ArrayList<>();
		if (tasks == null) return;
		if (tasks.length == 0) return;

		JavaCompiler compiler = JavaCompilerUtil.createJavaCompiler();

		Iterable<? extends JavaFileObject> compilationUnits =
				createJavaFileObjects(compiler, tasks);

		// die entsprechenden Prozessoren müssen hier generiert, compiliert und
		// instanziiert werden
		Iterable<? extends AbstractTaskProcessor> processors =
				createProcessors(compiler, tasks);

		JavaCompilerUtil.runProcessorsOnly(
				compiler, options, compilationUnits, processors);
	}

	private static Iterable<? extends JavaFileObject> createJavaFileObjects(
			JavaCompiler javaCompiler, AbstractAnnotatedElementsTask... tasks)
  {
		StandardJavaFileManager standardJavaFileManager =
				javaCompiler.getStandardFileManager(null, null, null);

		List<File> files = new ArrayList<>();

		for (AbstractAnnotatedElementsTask task : tasks)
		{
			files.addAll(task.getFileset().calculateFileSet());
			files.addAll(task.getCustomRootFiles().calculateFileSet());
		}

		return standardJavaFileManager.getJavaFileObjectsFromFiles(files);
  }
	
	private static class DynaTaskProcessorInfo
	{
		private AbstractAnnotatedElementsTask task;
		private RAMCompilerData ramCompilerData;
	}

	/**
	 * Each {@link AbstractAnnotatedElementsTask} has an individual file set (see {@link
	 * AbstractAnnotatedElementsTask#getFileset()}). This method generates, compiles and
	 * instantiates an {@link AbstractTaskProcessor} for each task and its
	 * respective {@link FileSet}.
	 *
	 * @param tasks
	 * @return {@link AbstractTaskProcessor}s for <code>tasks</code>
	 * @throws IOException
	 */
	private static Iterable<? extends AbstractTaskProcessor> createProcessors(
			JavaCompiler javaCompiler, AbstractAnnotatedElementsTask... tasks)
			throws IOException
  {
		List<AbstractTaskProcessor> result = new ArrayList<>();

		List<DynaTaskProcessorInfo> infos = createDynaTaskProcessorInfos(tasks);
		List<RAMCompilerData> ramCompilerDataItems = asRAMCompilerDataItems(infos);
		
		RAMCompiler.compile(ramCompilerDataItems);

		for (DynaTaskProcessorInfo info : infos)
		{
			Class<?> clazz;

			try
			{
				clazz =
						info.ramCompilerData
						    .getClassLoader()
						    .loadClass(info.ramCompilerData.getQualifiedTypeName());
			}
			catch (ClassNotFoundException e)
			{
				throw new IllegalStateException(
				    "failure creating instance of generated class", e);
			}

			Constructor<?> constructor;

			try
			{
				constructor = clazz.getConstructor(AbstractAnnotatedElementsTask.class);
			}
			catch (NoSuchMethodException | SecurityException e)
			{
				throw new IllegalStateException(
				    "failure getting constructor for generated class", e);
			}

			Object newInstance;

			try
			{
				newInstance = constructor.newInstance(info.task);
			}
			catch (InstantiationException | IllegalAccessException
			    | IllegalArgumentException | InvocationTargetException e)
			{
				throw new IOException(
						"failure during invocation of " + constructor.getName(), e);
			}

			if (newInstance instanceof AbstractTaskProcessor)
			{
				result.add((AbstractTaskProcessor) newInstance);
			}
		}

		return result;
  }

	private static List<RAMCompilerData> asRAMCompilerDataItems(
      List<DynaTaskProcessorInfo> infos)
  {
		List<RAMCompilerData> result = new ArrayList<>();
		
		for (DynaTaskProcessorInfo info : infos)
    {
			result.add(info.ramCompilerData);
    }
		
	  return result;
  }

	private static List<DynaTaskProcessorInfo> createDynaTaskProcessorInfos(
			AbstractAnnotatedElementsTask... tasks)
  {
		List<DynaTaskProcessorInfo> result = new ArrayList<>();

		String processornamePrefix = "__DynamicTaskProcessor__" + currentTimeMillis();

		int i = 0;
		
		for (AbstractAnnotatedElementsTask task : tasks)
		{
			// increment i directly after read access
			String name = processornamePrefix + (i++);
			String source = generateSourceForTaskProcessor(task, name);

//			LOGGER.debug("source code for task processor " + name + "\n" + source);

			DynaTaskProcessorInfo info = new DynaTaskProcessorInfo();

			info.task = task;
			info.ramCompilerData = new RAMCompilerData(name, source);
			
			result.add(info);
		}

		return result;
  }

	private static String generateSourceForTaskProcessor(
			AbstractAnnotatedElementsTask task, String classname)
	{
		List<String> supportedAnnotationTypes = new ArrayList<>();
		
		for (Class<?> annotationType : task.getAnnotationTypes())
		{
			supportedAnnotationTypes.add("\"" + annotationType.getName() + "\"");
//			supportedAnnotationTypes.add("\"" + normalizeClassName(annotationType) + "\"");
		}

		return
"@" + SupportedAnnotationTypes.class.getName() + "({ " + StringUtils.join(supportedAnnotationTypes, ", ") + " })"                    + lineSeparator() +
"public class " + classname + " extends " + AbstractTaskProcessor.class.getName()                                                    + lineSeparator() +
"{"                                                                                                                                  + lineSeparator() +
"  private " + AbstractAnnotatedElementsTask.class.getName() + " task;"                                                              + lineSeparator() +
"  public " + classname + "(" + AbstractAnnotatedElementsTask.class.getName() + " task)"                                             + lineSeparator() +
"  {"                                                                                                                                + lineSeparator() +
"    super(task);"                                                                                                                   + lineSeparator() +
"    this.task = task;"                                                                                                              + lineSeparator() +
"  }"                                                                                                                                + lineSeparator() +
"  @Override"                                                                                                                        + lineSeparator() +
"  protected " + Set.class.getName() + "<Class<? extends " + Annotation.class.getName() + ">> getSupposedSupportedAnnotationTypes()" + lineSeparator() +
"  {"                                                                                                                                + lineSeparator() +
"    return task.getAnnotationTypes();"                                                                                              + lineSeparator() +
"  }"                                                                                                                                + lineSeparator() +
"}";
	}
}